﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using Dexter.DataTransferObject;
using Dexter.Domain.Model;
using Dexter.Domain.Results;
using Dexter.Logger.Contracts;
using Dexter.Search.Lucene.Configuration;
using Dexter.Search.Lucene.DocumentDefinition;
using Dexter.Search.Lucene.Storage;
using Dexter.Services;
using Lucene.Net.Analysis;
using Lucene.Net.Analysis.Snowball;
using Lucene.Net.Analysis.Standard;
using Lucene.Net.Documents;
using Lucene.Net.Highlight;
using Lucene.Net.Index;
using Lucene.Net.QueryParsers;
using Lucene.Net.Search;
using Lucene.Net.Search.Similar;
using Lucene.Net.Util;

namespace Dexter.Search.Lucene {
	public class SearchService : ISearchService {
		readonly Analyzer analyzer;
		readonly global::Lucene.Net.Store.Directory directory;
		readonly ILogger logger;
		readonly ILuceneConfiguration luceneConfiguration;

		/// <summary>
		/// 	Initializes a new instance of the <see cref="T:System.Object" /> class.
		/// </summary>
		/// <param name="logger"> The logger. </param>
		/// <param name="luceneConfiguration"> The lucene configuration. </param>
		/// <param name="configurationService"> The configuration service. </param>
		/// <param name="luceneStorageService"> The lucene storage service. </param>
		public SearchService(ILogger logger, ILuceneConfiguration luceneConfiguration, IConfigurationService configurationService, ILuceneStorageService luceneStorageService) {
			this.luceneConfiguration = luceneConfiguration;
			this.logger = logger;
			analyzer = new StandardAnalyzer(global::Lucene.Net.Util.Version.LUCENE_29);
			directory = luceneStorageService.GetIndexDirectory();
		}

		#region ISearchService Members

		/// <summary>
		/// 	/// Search the full text index by query string
		/// </summary>
		/// <param name="pageIndex"> Index of the page. </param>
		/// <param name="pageSize"> Size of the page. </param>
		/// <param name="text"> The text. </param>
		/// <returns> </returns>
		public SearchResult Search(int pageIndex, int pageSize, string text) {
			if (String.IsNullOrEmpty(text)) {
				return new SearchResult(text, TimeSpan.Zero, null, null, null);
			}

			text = text.Normalize();
			text = text.Trim();

			using (Wrapper.SearchService searchService = new Wrapper.SearchService(directory)) {
				QueryParser parser = new QueryParser(global::Lucene.Net.Util.Version.LUCENE_29, FieldsName.Item.BODY, analyzer);
				parser.SetDefaultOperator(QueryParser.Operator.AND);
				Query bodyQuery = parser.Parse(text);

				string queryStringMerged = String.Format("({0}) OR ({1}) OR ({2})",
				                                         bodyQuery,
				                                         bodyQuery.ToString().Replace(FieldsName.Item.BODY, FieldsName.Item.TITLE),
				                                         bodyQuery.ToString().Replace(FieldsName.Item.BODY, FieldsName.Item.TAGS));

				Query query = parser.Parse(queryStringMerged);
				TimeSpan executionTime;
				int totalCount;

				Searcher searcher = searchService.GetSearcher();

				IEnumerable<SearchItem> data = ExecuteQuery(searcher, query, pageIndex, pageSize, text, out executionTime, out totalCount);

				string didYouMean = DidYouMead(text);

				if (!string.IsNullOrEmpty(didYouMean)) {
					Query query2 = MultiFieldQueryParser.Parse(global::Lucene.Net.Util.Version.LUCENE_29, didYouMean, new[]
					                                                                                                  	{
					                                                                                                  		FieldsName.Item.TITLE, FieldsName.Item.BODY
					                                                                                                  	}, new[]
					                                                                                                  	   	{
					                                                                                                  	   		BooleanClause.Occur.SHOULD, BooleanClause.Occur.MUST
					                                                                                                  	   	}, analyzer);

					TopDocs hits2 = searcher.Search(query2, null, 10);

					if (hits2.TotalHits < totalCount)
						didYouMean = null;
				}

				PagedResult<SearchItem> pageResult = new PagedResult<SearchItem>(pageIndex, pageSize, data, totalCount);
				SearchResult result = new SearchResult(text, executionTime, didYouMean, null, pageResult);

				searcher.Close();

				return result;
			}
		}

		/// <summary>
		/// 	Retrieve a set of related <see cref="Item" /> for the specified element.
		/// </summary>
		/// <param name="itemId"> The item id. </param>
		/// <param name="pageIndex"> Index of the page. </param>
		/// <param name="pageSize"> Size of the page. </param>
		/// <returns> </returns>
		public IPagedResult<SearchItem> ReleatedPost(int itemId, int pageIndex, int pageSize) {
			try {
				using (Wrapper.SearchService searchService = new Wrapper.SearchService(directory)) {
					Searcher searcher = searchService.GetSearcher();

					TermQuery q = new TermQuery(new Term(FieldsName.Item.ID, NumericUtils.IntToPrefixCoded(itemId)));

					TopDocs hits = searcher.Search(q, 1000);

					if (hits.ScoreDocs.Length < 1) {
						return new EmptyPagedResult<SearchItem>(pageIndex, pageSize);
					}

					int docNum = hits.ScoreDocs[0].doc;

					using (Wrapper.IndexService indexService = new Wrapper.IndexService(directory, analyzer)) {
						MoreLikeThis mlt = new MoreLikeThis(indexService.GetWriter().GetReader());
						mlt.SetAnalyzer(analyzer);
						mlt.SetFieldNames(new[] {FieldsName.Item.TITLE, FieldsName.Item.BODY, FieldsName.Item.TAGS});
						mlt.SetMinDocFreq(luceneConfiguration.MinimumDocumentFrequency);
						mlt.SetMinTermFreq(luceneConfiguration.MinimumTermFrequency);
						mlt.SetBoost(true);

						Query moreResultsQuery = mlt.Like(docNum);
						TimeSpan executionTime;
						int totalItems;
						IEnumerable<SearchItem> data = ExecuteQuery(searcher, moreResultsQuery, pageIndex, pageSize, null, out executionTime, out totalItems);

						return new PagedResult<SearchItem>(pageIndex, pageSize, data, totalItems);
					}
				}
			}
			catch (Exception e) {
				logger.Error(e.Message, e);
				return new EmptyPagedResult<SearchItem>(pageIndex, pageSize);
			}
		}

		/// <summary>
		/// 	Indexes the specified items.
		/// </summary>
		/// <param name="items"> The items. </param>
		public void IndexItems(IEnumerable<Item> items)
		{
			if (items == null)
				throw new ArgumentNullException();

			if (!items.Any()) {
				logger.Info("There is no item to index");
				return;
			}

			using (Wrapper.IndexService indexService = new Dexter.Search.Lucene.Wrapper.IndexService(directory, analyzer)) {
				ItemDefinition definition = new ItemDefinition(luceneConfiguration);
				IndexWriter writer = indexService.GetWriter();

				foreach (Item person in items) {
					Document indexDocument = definition.Convert(person);

					writer.AddDocument(indexDocument, analyzer);
				}
			}
		}

		/// <summary>
		/// 	Indexes the specified <see cref="Item" /> .
		/// </summary>
		/// <param name="item"> The item. </param>
		public void IndexItem(Item item) {
			if (item == null)
				throw new ArgumentNullException();

			IndexItems(new[] {item});
		}

		/// <summary>
		/// 	Removes the specified <see cref="Item" /> from the search index.
		/// </summary>
		/// <param name="item"> The item. </param>
		public void RemoveItem(Item item) {
			if (item == null)
				throw new ArgumentNullException();
			ItemDefinition personDefinition = new ItemDefinition(luceneConfiguration);

			using (Wrapper.IndexService indexService = new Wrapper.IndexService(directory, analyzer)) {
				indexService.GetWriter().DeleteDocuments(personDefinition.GetIndex(item));
			}
		}

		/// <summary>
		/// 	Gets the number of <see cref="Item" /> indexed.
		/// </summary>
		/// <returns> </returns>
		public int GetIndexedItemsCount() {
			using (Wrapper.IndexService indexService = new Wrapper.IndexService(directory, analyzer)) {
				return indexService.GetWriter().MaxDoc();
			}
		}

		#endregion

		IEnumerable<SearchItem> ExecuteQuery(Searcher searcher, Query queryOrig, int pageIndex, int pageSize, string searchText, out TimeSpan executionTime, out int totalCount) {
			int first = pageIndex*pageSize;
			int last = first + pageSize;

			Query isPublishedQuery = new TermQuery(new Term(FieldsName.Item.PUBLISH, "true"));

			BooleanQuery query = new BooleanQuery();

			query.Add(isPublishedQuery, BooleanClause.Occur.MUST);
			query.Add(queryOrig, BooleanClause.Occur.MUST);

			DateTime startTime = DateTime.Now;

			TopDocs hits = searcher.Search(query, 1000000);

			executionTime = DateTime.Now.Subtract(startTime);

			int length = hits.ScoreDocs.Length;
			float scoreNorm = 1.0f/hits.GetMaxScore();

			IList<SearchItem> returnValues = new List<SearchItem>();

			totalCount = length;

			for (int i = first; i <= last && i < hits.TotalHits; i++) {
				float score = hits.ScoreDocs[i].score*scoreNorm;
				ItemDefinition converter = new ItemDefinition(luceneConfiguration);

				SearchItem item = converter.Convert(searcher.Doc(hits.ScoreDocs[i].doc));
				item.Score = score;

				if (!string.IsNullOrEmpty(searchText)) {
					string text = searcher.Doc(hits.ScoreDocs[i].doc).Get(FieldsName.Item.BODY);
					item.HightligthText = GeneratePreviewText(query, text);
				}

				returnValues.Add(item);
			}

			return returnValues;
		}

		string DidYouMead(string searchText) {
			string[] values = DidYouMead(searchText, null, true);
			return values != null && values.Length > 0
			       	? values[0]
			       	: string.Empty;
		}

		string[] DidYouMead(string searchText, IndexReader indexReader, bool closeReader) {
			SpellChecker.Net.Search.Spell.SpellChecker spellChecker = new SpellChecker.Net.Search.Spell.SpellChecker(directory);
			IndexReader myIndexReader = indexReader ?? IndexReader.Open(directory, true);

			string[] returnValue = spellChecker.SuggestSimilar(searchText, 5);

			if (closeReader)
				myIndexReader.Close();

			return returnValue;
		}

		string GeneratePreviewText(Query q, string text) {
			QueryScorer scorer = new QueryScorer(q);
			SimpleHTMLFormatter formatter = new SimpleHTMLFormatter("<strong>", "</strong>");
			Highlighter highlighter = new Highlighter(formatter, scorer);
			highlighter.SetTextFragmenter(new SimpleFragmenter(200));
			TokenStream stream = analyzer.TokenStream(FieldsName.Item.BODY, new StringReader(text));
			TextFragment[] result = highlighter.GetBestTextFragments(stream, text, true, 5);

			StringBuilder sb = new StringBuilder();

			result.ForEach(x => sb.Append(x.ToString()).Append("---"));

			return sb.ToString();
		}
	}
}